﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Reflection;
using System.Text;
using Hyper.ComponentModel;
using Microsoft.Practices.EnterpriseLibrary.Data;
using PomodoroSuite.FW.Comun;
using PomodoroSuite.FW.Comun.Apoyo;
using PomodoroSuite.FW.Comun.Atributos;
using PomodoroSuite.FW.Comun.Caching;

namespace PomodoroSuite.FW.DAL
{
    /// <summary>
    /// Clase que se encarga del manejo de la interfaz con la BD
    /// </summary>
    public class DalGenerico<TEntidad> where TEntidad : BOGenerico, new()
    {
        #region " Campos "

        private Database _database = null;
        private Type _tipoEntidad = null;
        private string _nombreTabla = "";
        private string _nombreConexion = "";
        private PropertyDescriptorCollection _propiedades = null;
        private string _camposTabla = "";
        private string _idTabla = "";
        private DbTransaction _transaccion = null;

        #endregion

        #region " Constructores "

        /// <summary>
        /// Crea una nueva instancia de la clase derivada
        /// </summary>
        public DalGenerico()
        {
            _database = ConnectionSingleton.ObtenerInstancia("");
            ObtenerMetaDataTabla();
        }

        /// <summary>
        /// Crea una nueva instancia de la clase derivada
        /// </summary>
        /// <param name="nombreConexion">Nombre de la cadena de conexión, NO la cadena de conexión</param>
        public DalGenerico(string nombreConexion)
        {
            _database = ConnectionSingleton.ObtenerInstancia(nombreConexion);
            _tipoEntidad = typeof(TEntidad);
            _nombreTabla = ((MapearTabla)_tipoEntidad.GetCustomAttributes(typeof(MapearTabla), false)[0]).NombreTabla;
            _nombreConexion = nombreConexion;
        }

        /// <summary>
        /// Crea una nueva instancia de la clase derivada, hace referencia 
        /// a la BD que se pasa como parámetro
        /// </summary>
        /// <param name="database">Base de datos</param>
        public DalGenerico(Database database)
        {
            _database = database;
            _tipoEntidad = typeof(TEntidad);
            _nombreTabla = ((MapearTabla)_tipoEntidad.GetCustomAttributes(typeof(MapearTabla), false)[0]).NombreTabla;
        }

        #endregion

        #region " Propiedades "

        /// <summary>
        /// Conexión a al Base de datos
        /// </summary>
        public Database BD
        {
            get { return _database; }
        }

        /// <summary>
        /// Nombre de la conexion del archivo de configuración
        /// </summary>
        public string NombreConexion
        {
            get { return _nombreConexion; }
        }

        #endregion

        #region " Métodos "

        #region " Públicos "

        /// <summary>
        /// Devuelve el número de registros de la tabla
        /// </summary>
        /// <returns></returns>
        public int ContarRegistros()
        {
            int nroRegistros = 0;
            string sql = "Select count(Id" + _tipoEntidad.Name + ") From " + _nombreTabla;
            nroRegistros = (int)_database.ExecuteScalar(CommandType.Text, sql);

            return nroRegistros;
        }

        /// <summary>
        /// Devuelve una entidad por su ID
        /// </summary>
        /// <param name="idEntidad">Id de la entidad a obtener</param>
        /// <returns></returns>
        public TEntidad Obtener(int idEntidad, params bool[] hijos)
        {
            TEntidad entidad = default(TEntidad);
            using (IDataReader reader = _database.ExecuteReader(CommandType.Text, "Select * From " + _nombreTabla + " Where " + _idTabla + " = " + idEntidad))
            {
                if (reader.Read())
                    entidad = CargarEntidad(reader, hijos);
            }

            return entidad;
        }

        /// <summary>
        /// Obtiene un listado de todos los registros de una tabla
        /// </summary>
        /// <returns></returns>
        public List<TEntidad> ObtenerListado(params bool[] hijos)
        {
            List<TEntidad> listaEntidades = new List<TEntidad>();
            using (IDataReader reader = _database.ExecuteReader(CommandType.Text, "Select * From " + _nombreTabla))
            {
                while (reader.Read())
                    listaEntidades.Add(CargarEntidad(reader, hijos));
            }

            return listaEntidades;
        }

        /// <summary>
        /// Obtiene un listado filtrado
        /// </summary>
        /// <param name="filtros"></param>
        /// <returns></returns>
        public List<TEntidad> ObtenerListadoFiltrado(List<Filtro> filtros, params bool[] hijos)
        {
            List<TEntidad> listaEntidades = new List<TEntidad>();
            StringBuilder sentencia = new StringBuilder();
            sentencia.Append("Select * From " + _nombreTabla);
            if (filtros.Count > 0)
                sentencia.Append(" Where ");
            sentencia.Append(ObtenerFiltros(filtros));
            Query query = new Query();
            query.Sentencia = sentencia.ToString();
            query.Tipo = CommandType.Text;
            query.Parametros = new List<Parametro>();

            foreach (Filtro filtro in filtros)
            {
                if (filtro.TipoBD.HasValue)
                    query.Parametros.Add(new Parametro(filtro.NombreVariable, filtro.Valor, filtro.TipoBD.Value));
                else
                    query.Parametros.Add(new Parametro(filtro.NombreVariable, filtro.Valor));
            }

            return EjecutarQuery(query, hijos);
        }

        /// <summary>
        /// Obtiene un listado paginado
        /// </summary>
        /// <param name="tamanioPagina">Tamaño de la pagína, por defecto 10 registros</param>
        /// <param name="indicePagina">Índice de la pagina, comienza en 1</param>
        /// <returns></returns>
        public List<TEntidad> ObtenerListadoPaginado(int tamanioPagina, int indicePagina, params bool[] hijos)
        {
            return ListadoPaginadoOrdenado(tamanioPagina, indicePagina, new Orden(), hijos);
        }

        /// <summary>
        /// Obtiene un listado paginado y ordenado
        /// </summary>
        /// <param name="tamanioPagina">Tamaño de la pagína, por defecto 10 registros</param>
        /// <param name="indicePagina">Índice de la pagina, comienza en 1</param>
        /// <param name="orden">Ordenación del resultado de la consulta (ORDER BY)</param>
        /// <returns></returns>
        public List<TEntidad> ObtenerListadoPaginadoOrdenado(int tamanioPagina, int indicePagina, Orden orden, params bool[] hijos)
        {
            return ListadoPaginadoOrdenado(tamanioPagina, indicePagina, orden, hijos);
        }

        /// <summary>
        /// Este método permite ejecutar una sentencia para obtener un listado de entidades.
        /// NOTA: Si los campos de la sentencia SELECT no coinciden con las propiedades de la entidad
        /// se producirá un error.
        /// </summary>
        /// <param name="query">Query a ejecutar</param>
        /// <returns></returns>
        public List<TEntidad> EjecutarQuery(Query query, params bool[] hijos)
        {
            List<TEntidad> listaEntidades = new List<TEntidad>();
            DbCommand comando = _database.DbProviderFactory.CreateCommand();
            try
            {
                comando.CommandText = query.Sentencia;
                comando.CommandType = query.Tipo;
                if (query.Parametros.Count > 0)
                {
                    foreach (Parametro parametro in query.Parametros)
                        _database.AddInParameter(comando, parametro.NombreParametro, TypeToDbType(parametro.Valor.GetType()), (parametro.Valor is Enum) ? parametro.Valor.ToString() : parametro.Valor);
                }

                using (IDataReader reader = _database.ExecuteReader(comando))
                {
                    while (reader.Read())
                        listaEntidades.Add(CargarEntidad(reader, hijos));
                }
            }
            catch (Exception exc)
            {
                throw exc;
            }
            finally
            {
                if (comando.Connection.State != ConnectionState.Closed)
                {
                    comando.Connection.Close();
                    comando.Dispose();
                }
            }

            return listaEntidades;
        }

        /// <summary>
        /// Devuelve el último id creado para la tabla
        /// </summary>
        /// <returns></returns>
        public int ObtenerUltimoID()
        {
            string sentencia = "SELECT IDENT_CURRENT('" + _nombreTabla + "')";
            int id = 0;
            int.TryParse(_database.ExecuteScalar(CommandType.Text, sentencia).ToString(), out id);
            return id;
        }

        /// <summary>
        /// Inserta una entidad en la BD
        /// </summary>
        /// <param name="entidad">Entidad a insertar</param>
        /// <returns></returns>
        public int Insertar(TEntidad entidad)
        {
            Query query = new Query();
            query.Tipo = CommandType.Text;
            query.Sentencia = "INSERT INTO " + _nombreTabla + " (" + _camposTabla + ") VALUES (@" + _camposTabla.Replace(",", ",@") + ")";
            foreach (var campo in _camposTabla.Split(','))
            {
                object valor = _propiedades[campo].GetValue(entidad);
                if (valor == null)
                    query.Parametros.Add(new Parametro(campo, DBNull.Value));
                else if (valor.GetType().BaseType.Equals(typeof(Enum)))
                    query.Parametros.Add(new Parametro(campo, valor.ToString()));
                else
                    query.Parametros.Add(new Parametro(campo, valor));
            }
            DbConnection conexion = null;
            using (conexion = BD.CreateConnection())
            {
                conexion.Open();
                using (DbTransaction transaccion = conexion.BeginTransaction(IsolationLevel.Serializable))
                {
                    try
                    {
                        EjecutarNonQuery(query, transaccion);
                        transaccion.Commit();
                    }
                    catch (Exception exc)
                    {
                        transaccion.Rollback();
                        throw exc;
                    }
                }
            }

            _propiedades[_idTabla].SetValue(entidad, ObtenerUltimoID());
            return (int)_propiedades[_idTabla].GetValue(entidad);
        }

        /// <summary>
        /// Actualiza una entidad
        /// </summary>
        /// <param name="entidad">Entidad a actualizar</param>
        public void Actualizar(TEntidad entidad)
        {
            Query query = new Query();
            query.Tipo = CommandType.Text;
            StringBuilder sentencia = new StringBuilder();
            sentencia.Append("UPDATE " + _nombreTabla + " SET ");

            foreach (var campo in _camposTabla.Split(','))
            {
                sentencia.Append(campo + "=@" + campo + ",");
                object valor = _propiedades[campo].GetValue(entidad);
                if (valor == null)
                    query.Parametros.Add(new Parametro(campo, DBNull.Value));
                else if (valor.GetType().BaseType.Equals(typeof(Enum)))
                    query.Parametros.Add(new Parametro(campo, valor.ToString()));
                else
                    query.Parametros.Add(new Parametro(campo, valor));
            }

            query.Sentencia = sentencia.ToString().TrimEnd(',') + " WHERE " + _idTabla + "=" + _propiedades[_idTabla].GetValue(entidad);
            DbConnection conexion = null;
            using (conexion = BD.CreateConnection())
            {
                conexion.Open();
                using (DbTransaction transaccion = conexion.BeginTransaction(IsolationLevel.Serializable))
                {
                    try
                    {
                        EjecutarNonQuery(query, transaccion);
                        transaccion.Commit();
                    }
                    catch (Exception exc)
                    {
                        transaccion.Rollback();
                        throw exc;
                    }
                }
            }
        }

        /// <summary>
        /// Elimina una entidad
        /// </summary>
        /// <param name="idEntidad">Id de la entidad a eliminar</param>
        public void Eliminar(int idEntidad)
        {
            Query query = new Query();
            query.Tipo = CommandType.Text;
            query.Sentencia = "DELETE FROM " + _nombreTabla + " WHERE " + _idTabla + "=@IdTabla";
            query.Parametros.Add(new Parametro("IdTabla", idEntidad));

            DbConnection conexion = null;
            using (conexion = BD.CreateConnection())
            {
                conexion.Open();
                using (DbTransaction transaccion = conexion.BeginTransaction(IsolationLevel.Serializable))
                {
                    try
                    {
                        EjecutarNonQuery(query, transaccion);
                        transaccion.Commit();
                    }
                    catch (Exception exc)
                    {
                        transaccion.Rollback();
                        throw exc;
                    }
                }
            }
        }

        #endregion

        #region " Privados "

        /// <summary>
        /// Obtiene toda la información de la tabla
        /// </summary>
        private void ObtenerMetaDataTabla()
        {
            HyperTypeDescriptionProvider.Add(typeof(TEntidad));

            _tipoEntidad = typeof(TEntidad);
            _nombreTabla = ((MapearTabla)_tipoEntidad.GetCustomAttributes(typeof(MapearTabla), false)[0]).NombreTabla;
            _propiedades = TypeDescriptor.GetProperties(typeof(TEntidad));

            StringBuilder campos = new StringBuilder();
            PropertyInfo[] propiedadesReflection = _tipoEntidad.GetProperties();
            CampoTabla campoTabla = null;

            foreach (var propiedad in propiedadesReflection)
            {
                campoTabla = propiedad.GetCustomAttributes(typeof(CampoTabla), false).FirstOrDefault() as CampoTabla;

                if (campoTabla != null)
                {
                    if (campoTabla.LlavePrimaria)
                        _idTabla = propiedad.Name;
                    else
                        campos.Append(propiedad.Name + ",");
                }
            }

            _camposTabla = campos.ToString().TrimEnd(',');
            campos = null;
        }

        private List<TEntidad> ListadoPaginadoOrdenado(int tamanioPagina, int indicePagina, Orden orden, params bool[] hijos)
        {
            long desdeRegistro = 0;
            long hastaRegistro = 0;

            if (indicePagina <= 0)
                indicePagina = 1;
            if (tamanioPagina <= 0)
                tamanioPagina = 10;

            desdeRegistro = ((indicePagina - 1) * tamanioPagina) + 1;
            hastaRegistro = indicePagina * tamanioPagina;

            /// Se arma la consulta para el paginado
            StringBuilder consulta = new StringBuilder();
            string camposEntidad = ObtenerCamposEntidad();
            consulta.Append("WITH TablaTemporal AS ");
            consulta.Append("(SELECT " + camposEntidad + ",ROW_NUMBER() OVER(ORDER BY " +
                ((string.IsNullOrEmpty(orden.NombreColumna)) ? _idTabla : orden.NombreColumna + " " + (orden.OrdenPor == TipoOrden.Asc ? "ASC" : "DESC")) + ") as NroFila FROM " + _nombreTabla + ")");
            consulta.Append("SELECT " + camposEntidad + " FROM TablaTemporal");
            consulta.Append(" WHERE NroFila BETWEEN " + desdeRegistro + " AND " + hastaRegistro);

            /// Ejecución de la consulta
            List<TEntidad> listaEntidades = new List<TEntidad>();
            using (IDataReader reader = _database.ExecuteReader(CommandType.Text, consulta.ToString()))
            {
                while (reader.Read())
                    listaEntidades.Add(CargarEntidad(reader, hijos));
            }
            return listaEntidades;
        }

        /// <summary>
        /// Obtiene los campos
        /// </summary>
        /// <returns></returns>
        private string ObtenerCamposEntidad()
        {
            if (CacheManager.Get("Propiedades" + _tipoEntidad.Name) == null)
            {
                StringBuilder campos = new StringBuilder();

                PropertyInfo[] propiedades = _tipoEntidad.GetProperties();
                foreach (PropertyInfo propiedad in propiedades)
                {
                    CampoTabla[] atributos = (CampoTabla[])propiedad.GetCustomAttributes(typeof(CampoTabla), false);
                    if (atributos.Length > 0)
                        campos.Append(propiedad.Name + ",");
                }
                CacheManager.AddToCache("Propiedades" + _tipoEntidad.Name, campos.ToString().Substring(0, campos.Length - 1), 30);
                return campos.ToString().Substring(0, campos.Length - 1);
            }
            else
                return CacheManager.Get("Propiedades" + _tipoEntidad.Name).ToString();
        }

        /// <summary>
        /// Devuelve una cadena con la sentencia de filtrado (Where ....)
        /// </summary>
        /// <param name="filtros">Filtros de los cuales se generará la sentencia</param>
        /// <returns></returns>
        private string ObtenerFiltros(List<Filtro> filtros)
        {
            StringBuilder sentencia = new StringBuilder();
            if (filtros != null && filtros.Count > 0)
            {
                bool primerElemento = true;
                foreach (Filtro filtro in filtros)
                {
                    if (!primerElemento)
                        sentencia.Append((filtro.FiltroConcatenacion == TipoFiltro.And) ? " And " : " Or ");
                    sentencia.Append(filtro.NombreCampo);
                    switch (filtro.OperacionLogica)
                    {
                        case Operacion.Igual:
                            sentencia.Append(" = ");
                            break;
                        case Operacion.NoIgual:
                            sentencia.Append(" <> ");
                            break;
                        case Operacion.MayorIgual:
                            sentencia.Append(" >= ");
                            break;
                        case Operacion.MenorIgual:
                            sentencia.Append(" <= ");
                            break;
                        case Operacion.Mayor:
                            sentencia.Append(" > ");
                            break;
                        case Operacion.Menor:
                            sentencia.Append(" < ");
                            break;
                        case Operacion.Like:
                            sentencia.Append(" Like ");
                            break;
                    }
                    if (filtro.OperacionLogica == Operacion.Like)
                        sentencia.Append("'%' + @" + filtro.NombreVariable + " + '%'");
                    else
                        sentencia.Append("@" + filtro.NombreVariable);
                    primerElemento = false;
                }
            }
            return sentencia.ToString();
        }

        private string ValorCorrecto(Filtro filtro, Operacion operacion)
        {
            switch (filtro.ValorTipo)
            {
                case TipoValor.Cadena:
                    if (operacion == Operacion.Like)
                        return "'%" + filtro.Valor + "%'";
                    else
                        return "'" + filtro.Valor + "'";
                case TipoValor.Numero:
                    return filtro.Valor.ToString();
                case TipoValor.Fecha:
                    return "'" + DateTime.Parse(filtro.Valor.ToString()).ToShortDateString() + "'";
                case TipoValor.Booleano:
                    return filtro.Valor.ToString();
            }

            return "";
        }

        #endregion

        #region " Protegidos "

        /// <summary>
        /// Método virutal que se encarga de vaciar los datos del datareader a un objeto de negocio
        /// </summary>
        /// <param name="reader">DataReader del cual cargar los datos</param>
        /// <param name="hijos">Listo de hijos para cargar junto con el padre</param>
        /// <returns></returns>
        private TEntidad CargarEntidad(IDataReader reader, params bool[] hijos)
        {
            TEntidad entidad = new TEntidad();
            for (int i = 0; i < reader.FieldCount; i++)
            {
                if (_propiedades[reader.GetName(i)].PropertyType.BaseType.Equals(typeof(Enum)))
                    _propiedades[reader.GetName(i)].SetValue(entidad, Enum.Parse(_propiedades[reader.GetName(i)].PropertyType, reader.GetValue(i).ToString()));
                else
                {
                    if (reader.GetValue(i) is DBNull)
                        _propiedades[reader.GetName(i)].SetValue(entidad, null);
                    else
                        _propiedades[reader.GetName(i)].SetValue(entidad, reader.GetValue(i));
                }
            }

            return entidad;
        }

        /// <summary>
        /// Ejecuta una sentencia SQL
        /// </summary>
        /// <param name="query">Objeto que contiene la información de la sentencia a ejecutarse</param>
        /// <param name="transaccion">
        /// Transacción dentro de la cual se ejecuta la sentencia, si esta fuera null
        /// se crea una nueva transaccion antes de ejecutar la sentencia
        /// </param>
        protected void EjecutarNonQuery(Query query, DbTransaction transaccion)
        {
            try
            {
                DbCommand comando;
                if (transaccion == null)
                    comando = BD.DbProviderFactory.CreateCommand();
                else
                {
                    comando = transaccion.Connection.CreateCommand();
                    comando.Transaction = transaccion;
                }
                comando.CommandText = query.Sentencia;
                comando.CommandType = query.Tipo;

                if (query.Parametros.Count > 0)
                {
                    foreach (Parametro parametro in query.Parametros)
                    {
                        DbParameter parametroIn = comando.CreateParameter();
                        parametroIn.ParameterName = parametro.NombreParametro;
                        parametroIn.Value = parametro.Valor;
                        if (parametro.Valor != DBNull.Value && parametro.Tipo.HasValue)
                            parametroIn.DbType = TypeToDbType(parametro.Valor.GetType());
                        comando.Parameters.Add(parametroIn);
                    }
                }

                if (transaccion == null)
                    _database.ExecuteNonQuery(comando);
                else
                    comando.ExecuteNonQuery();
            }
            catch (Exception exc)
            {
                throw exc;
            }
        }

        /// <summary>
        /// Convierte un tipo .Net a un tipo DbType
        /// </summary>
        /// <param name="t">Tipo a convertir</param>
        /// <returns></returns>
        public static DbType TypeToDbType(Type t)
        {
            switch (t.Name)
            {
                case "Int16":
                    return DbType.Int16;
                case "Int32":
                    return DbType.Int32;
                case "Int64":
                    return DbType.Int64;
                case "Byte[]":
                    return DbType.Binary;
                case "Boolean":
                    return DbType.Boolean;
                case "String":
                    return DbType.String;
                case "DateTime":
                    return DbType.DateTime;
                case "Decimal":
                    return DbType.Decimal;
                case "Double":
                    return DbType.Double;
                case "Single":
                    return DbType.Single;
                case "Byte":
                    return DbType.Byte;
                case "Guid":
                    return DbType.Guid;
                case "Enum":
                case "enum":
                    return DbType.String;
                default:
                    if (t.BaseType == typeof(Enum))
                        return DbType.String;
                    else
                        return DbType.Object;
            }
        }

        #endregion

        #endregion
    }
}
